// Copyright 2013-2014 The Rust Project Developers.
// Copyright 2018 The Uuid Project Developers.
//
// See the COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! [`Uuid`] parsing constructs and utilities.
//!
//! [`Uuid`]: ../struct.Uuid.html

pub(crate) mod error;
pub(crate) use self::error::Error;

use crate::{adapter, Uuid};

/// Check if the length matches any of the given criteria lengths.
fn len_matches_any(len: usize, crits: &[usize]) -> bool {
    for crit in crits {
        if len == *crit {
            return true;
        }
    }

    false
}

/// Check if the length matches any criteria lengths in the given range
/// (inclusive).
#[allow(dead_code)]
fn len_matches_range(len: usize, min: usize, max: usize) -> bool {
    for crit in min..=max {
        if len == crit {
            return true;
        }
    }

    false
}

// Accumulated length of each hyphenated group in hex digits.
const ACC_GROUP_LENS: [usize; 5] = [8, 12, 16, 20, 32];

// Length of each hyphenated group in hex digits.
const GROUP_LENS: [usize; 5] = [8, 4, 4, 4, 12];

impl Uuid {
    /// Parses a `Uuid` from a string of hexadecimal digits with optional
    /// hyphens.
    ///
    /// Any of the formats generated by this module (simple, hyphenated, urn)
    /// are supported by this parsing function.
    pub fn parse_str(mut input: &str) -> Result<Uuid, crate::Error> {
        // Ensure length is valid for any of the supported formats
        let len = input.len();

        if len == adapter::Urn::LENGTH && input.starts_with("urn:uuid:") {
            input = &input[9..];
        } else if !len_matches_any(
            len,
            &[adapter::Hyphenated::LENGTH, adapter::Simple::LENGTH],
        ) {
            Err(Error::InvalidLength {
                expected: error::ExpectedLength::Any(&[
                    adapter::Hyphenated::LENGTH,
                    adapter::Simple::LENGTH,
                ]),
                found: len,
            })?;
        }

        // `digit` counts only hexadecimal digits, `i_char` counts all chars.
        let mut digit = 0;
        let mut group = 0;
        let mut acc = 0;
        let mut buffer = [0u8; 16];

        for (i_char, chr) in input.bytes().enumerate() {
            if digit as usize >= adapter::Simple::LENGTH && group != 4 {
                if group == 0 {
                    Err(Error::InvalidLength {
                        expected: error::ExpectedLength::Any(&[
                            adapter::Hyphenated::LENGTH,
                            adapter::Simple::LENGTH,
                        ]),
                        found: len,
                    })?;
                }

                Err(Error::InvalidGroupCount {
                    expected: error::ExpectedLength::Any(&[1, 5]),
                    found: group + 1,
                })?;
            }

            if digit % 2 == 0 {
                // First digit of the byte.
                match chr {
                    // Calulate upper half.
                    b'0'..=b'9' => acc = chr - b'0',
                    b'a'..=b'f' => acc = chr - b'a' + 10,
                    b'A'..=b'F' => acc = chr - b'A' + 10,
                    // Found a group delimiter
                    b'-' => {
                        // TODO: remove the u8 cast
                        // BODY: this only needed until we switch to
                        //       ParseError
                        if ACC_GROUP_LENS[group] as u8 != digit {
                            // Calculate how many digits this group consists of
                            // in the input.
                            let found = if group > 0 {
                                // TODO: remove the u8 cast
                                // BODY: this only needed until we switch to
                                //       ParseError
                                digit - ACC_GROUP_LENS[group - 1] as u8
                            } else {
                                digit
                            };

                            Err(Error::InvalidGroupLength {
                                expected: error::ExpectedLength::Exact(
                                    GROUP_LENS[group],
                                ),
                                found: found as usize,
                                group,
                            })?;
                        }
                        // Next group, decrement digit, it is incremented again
                        // at the bottom.
                        group += 1;
                        digit -= 1;
                    }
                    _ => {
                        Err(Error::InvalidCharacter {
                            expected: "0123456789abcdefABCDEF-",
                            found: input[i_char..].chars().next().unwrap(),
                            index: i_char,
                            urn: error::UrnPrefix::Optional,
                        })?;
                    }
                }
            } else {
                // Second digit of the byte, shift the upper half.
                acc *= 16;
                match chr {
                    b'0'..=b'9' => acc += chr - b'0',
                    b'a'..=b'f' => acc += chr - b'a' + 10,
                    b'A'..=b'F' => acc += chr - b'A' + 10,
                    b'-' => {
                        // The byte isn't complete yet.
                        let found = if group > 0 {
                            // TODO: remove the u8 cast
                            // BODY: this only needed until we switch to
                            //       ParseError
                            digit - ACC_GROUP_LENS[group - 1] as u8
                        } else {
                            digit
                        };

                        Err(Error::InvalidGroupLength {
                            expected: error::ExpectedLength::Exact(
                                GROUP_LENS[group],
                            ),
                            found: found as usize,
                            group,
                        })?;
                    }
                    _ => {
                        Err(Error::InvalidCharacter {
                            expected: "0123456789abcdefABCDEF-",
                            found: input[i_char..].chars().next().unwrap(),
                            index: i_char,
                            urn: error::UrnPrefix::Optional,
                        })?;
                    }
                }
                buffer[(digit / 2) as usize] = acc;
            }
            digit += 1;
        }

        // Now check the last group.
        // TODO: remove the u8 cast
        // BODY: this only needed until we switch to
        //       ParseError
        if ACC_GROUP_LENS[4] as u8 != digit {
            Err(Error::InvalidGroupLength {
                expected: error::ExpectedLength::Exact(GROUP_LENS[4]),
                found: (digit as usize - ACC_GROUP_LENS[3]),
                group,
            })?;
        }

        Ok(Uuid::from_bytes(buffer))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{adapter, std::string::ToString, test_util};

    #[test]
    fn test_parse_uuid_v4() {
        const EXPECTED_UUID_LENGTHS: error::ExpectedLength =
            error::ExpectedLength::Any(&[
                adapter::Hyphenated::LENGTH,
                adapter::Simple::LENGTH,
            ]);

        const EXPECTED_GROUP_COUNTS: error::ExpectedLength =
            error::ExpectedLength::Any(&[1, 5]);

        const EXPECTED_CHARS: &'static str = "0123456789abcdefABCDEF-";

        // Invalid
        assert_eq!(
            Uuid::parse_str("").map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 0,
            })
        );

        assert_eq!(
            Uuid::parse_str("!").map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 1
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faa-B6BF-329BF39FA1E45")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 37,
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faa-BBF-329BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 35
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faa-BGBF-329BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidCharacter {
                expected: EXPECTED_CHARS,
                found: 'G',
                index: 20,
                urn: error::UrnPrefix::Optional,
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2F4faaFB6BFF329BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidGroupCount {
                expected: EXPECTED_GROUP_COUNTS,
                found: 2
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faaFB6BFF329BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidGroupCount {
                expected: EXPECTED_GROUP_COUNTS,
                found: 3,
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faa-B6BFF329BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidGroupCount {
                expected: EXPECTED_GROUP_COUNTS,
                found: 4,
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faa")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 18,
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faaXB6BFF329BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidCharacter {
                expected: EXPECTED_CHARS,
                found: 'X',
                index: 18,
                urn: error::UrnPrefix::Optional,
            })
        );

        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB-24fa-eB6BFF32-BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidGroupLength {
                expected: error::ExpectedLength::Exact(4),
                found: 3,
                group: 1,
            })
        );
        // (group, found, expecting)
        //
        assert_eq!(
            Uuid::parse_str("01020304-1112-2122-3132-41424344")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidGroupLength {
                expected: error::ExpectedLength::Exact(12),
                found: 8,
                group: 4,
            })
        );

        assert_eq!(
            Uuid::parse_str("67e5504410b1426f9247bb680e5fe0c")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 31,
            })
        );

        assert_eq!(
            Uuid::parse_str("67e5504410b1426f9247bb680e5fe0c88")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 33,
            })
        );

        assert_eq!(
            Uuid::parse_str("67e5504410b1426f9247bb680e5fe0cg8")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 33,
            })
        );

        assert_eq!(
            Uuid::parse_str("67e5504410b1426%9247bb680e5fe0c8")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidCharacter {
                expected: EXPECTED_CHARS,
                found: '%',
                index: 15,
                urn: error::UrnPrefix::Optional,
            })
        );

        assert_eq!(
            Uuid::parse_str("231231212212423424324323477343246663")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 36,
            })
        );

        // Valid
        assert!(Uuid::parse_str("00000000000000000000000000000000").is_ok());
        assert!(Uuid::parse_str("67e55044-10b1-426f-9247-bb680e5fe0c8").is_ok());
        assert!(Uuid::parse_str("F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4").is_ok());
        assert!(Uuid::parse_str("67e5504410b1426f9247bb680e5fe0c8").is_ok());
        assert!(Uuid::parse_str("01020304-1112-2122-3132-414243444546").is_ok());
        assert!(Uuid::parse_str(
            "urn:uuid:67e55044-10b1-426f-9247-bb680e5fe0c8"
        )
        .is_ok());

        // Nil
        let nil = Uuid::nil();
        assert_eq!(
            Uuid::parse_str("00000000000000000000000000000000").unwrap(),
            nil
        );
        assert_eq!(
            Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(),
            nil
        );

        // Round-trip
        let uuid_orig = test_util::new();
        let orig_str = uuid_orig.to_string();
        let uuid_out = Uuid::parse_str(&orig_str).unwrap();
        assert_eq!(uuid_orig, uuid_out);

        // Test error reporting
        assert_eq!(
            Uuid::parse_str("67e5504410b1426f9247bb680e5fe0c")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidLength {
                expected: EXPECTED_UUID_LENGTHS,
                found: 31,
            })
        );
        assert_eq!(
            Uuid::parse_str("67e550X410b1426f9247bb680e5fe0cd")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidCharacter {
                expected: EXPECTED_CHARS,
                found: 'X',
                index: 6,
                urn: error::UrnPrefix::Optional,
            })
        );
        assert_eq!(
            Uuid::parse_str("67e550-4105b1426f9247bb680e5fe0c")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidGroupLength {
                expected: error::ExpectedLength::Exact(8),
                found: 6,
                group: 0,
            })
        );
        assert_eq!(
            Uuid::parse_str("F9168C5E-CEB2-4faa-B6BF1-02BF39FA1E4")
                .map_err(crate::Error::expect_parser),
            Err(Error::InvalidGroupLength {
                expected: error::ExpectedLength::Exact(4),
                found: 5,
                group: 3,
            })
        );
    }
}
